feat(slack-app phase 2): Slack OAuth install flow + workspace persistence#26
Merged
feat(slack-app phase 2): Slack OAuth install flow + workspace persistence#26
Conversation
…ence
Second slice of the Slack app work. Stands up the OAuth handshake,
workspace persistence with encrypted bot tokens, and the four routes
the dashboard needs to render the connect/disconnect/status UI.
Crypto refactor (no behaviour change):
- Extract encryptString / decryptString from llm-key-crypto.ts as the
generic primitive (HKDF-SHA256 sub-key derivation + AES-256-GCM).
- LLM key wrappers (encryptLlmKey / decryptLlmKey) now build their salt
from the existing KeyScope and call into the generic primitive.
- New Slack wrappers (encryptSlackBotToken / decryptSlackBotToken) use
salt='slack:<slack_team_id>' so each workspace's token has a distinct
derived sub-key.
slack-workspace-service.ts:
- upsertSlackWorkspace (create or re-install), getActiveWorkspaceForTeam
/ forUser, getWorkspaceBySlackTeamId, getWorkspaceWithToken (decrypts
the bot token; only used when actually calling Slack), softDelete-
Workspace. All audit-logged: slack.installed, slack.reinstalled,
slack.uninstalled.
slack-oauth.ts:
- signOauthState / verifyOauthState — opaque, AEAD-encrypted state with
a 10-min TTL, carries the originating reflect_user_id. Reuses the
master encryption key (no extra env var).
- loadSlackOauthConfig + getActiveSlackConfig — reads REFLECT_DEV_SLACK_*
or REFLECT_PROD_SLACK_* depending on RM_SLACK_ENV.
- buildInstallUrl — composes the slack.com/oauth/v2/authorize URL with
the bot scope set the v1 manifest declares.
- exchangeOauthCode — calls slack.com/api/oauth.v2.access with the code,
returns workspace metadata + bot token on success.
- verifySlackSignature — HMAC-SHA256 with timing-safe compare and a
5-min timestamp tolerance, ready for /slack/events in phase 3.
- revokeSlackToken — best-effort auth.revoke during uninstall.
HTTP routes (4 new, all admin-gated except the public callback):
- POST /slack/install-url -> { url, state, redirect_uri }
- GET /slack/oauth/callback -> 302 to dashboard ?installed=1
(signed state replaces auth)
- GET /slack/status -> { configured, workspace | null }
- DELETE /slack/uninstall -> 200 + audit, best-effort revoke
The /slack/oauth/callback bypass in the global Bearer-auth hook is
explicit; the signed state IS the auth.
Tests: +20 (335 total, all green).
- State HMAC roundtrip / tampering / expiry / bogus input.
- verifySlackSignature: valid / wrong sig / stale / future / non-numeric.
- POST /slack/install-url: 403 for non-admin, returns slack.com URL with
the state echoed and parseable.
- GET /slack/status: 403 for non-admin, null when nothing installed,
populated after a direct upsert (cross-process master key shared via
.test-server.json so this works).
- GET /slack/oauth/callback error paths: missing code, bad state, slack
error param all redirect to dashboard with error= query param.
- DELETE /slack/uninstall: 403 / 404 / 200 + audit event verified.
Refs: parent memory d959bc61 (Eng Plan: Slack App v1) + 3a2f27a3
(Phase 1 shipped).
Made-with: Cursor
The secret-scanning CI job matches the literal regex `xoxb-[a-zA-Z0-9-]+` even on obviously-fake test fixtures. Build the fake tokens at runtime so the source contains no matching literal, while the runtime values remain identical and the tests still verify the same invariants. Also tightens the JSON-response negative assertion to look for any `xoxb-` prefix (also runtime-built), not just the specific fixture. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 of the Slack app (full design: `docs/eng-plan-slack-app-v1.md`, parent memory `d959bc61`). Stands up:
Phase 3 (the actual /slack/events webhook + agent loop) is the next chunk.
Crypto refactor (no behaviour change to LLM keys)
Extracted `encryptString` / `decryptString` from `llm-key-crypto.ts` as the generic primitive. LLM key wrappers reuse it via `KeyScope` salt; Slack bot token wrappers use `slack:<slack_team_id>` salt. The `llm-keys` integration tests pass unchanged.
New files
New routes
The callback bypass in the global Bearer-auth hook is explicit and called out in the diff. Signed state IS the auth.
Tests
335/335 backend tests green (was 315; +20 new). Covers state HMAC roundtrip / tampering / expiry, Slack signature verification (valid / wrong / stale / future), all 4 HTTP routes (admin gating, success paths, error paths), audit-event recording.
Manual smoke (after dev deploy)
Companion PR
Dashboard PR (proxies + UI) lands separately. Both must merge for the full UX.
Required env (already set on dev rm01 `.env`)
Test plan
Made with Cursor